package com.nai365.nart.blockchain.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.nai365.nart.blockchain.dto.*;
import com.nai365.nart.blockchain.entity.DdcBalance;
import com.nai365.nart.blockchain.entity.DdcTransfer;
import com.nai365.nart.blockchain.entity.DdcTransferRequest;
import com.nai365.nart.blockchain.entity.Transaction;
import com.nai365.nart.blockchain.mapper.AccountMapper;
import com.nai365.nart.blockchain.mapper.DdcBalanceMapper;
import com.nai365.nart.blockchain.mapper.DdcTransferRequestMapper;
import com.nai365.nart.blockchain.mapper.TransactionMapper;
import com.nai365.nart.blockchain.service.AccountService;
import com.nai365.nart.blockchain.service.DDCService;
import com.nai365.nart.blockchain.service.DdcBalanceService;
import com.nai365.nart.blockchain.service.DdcTransferService;
import com.reddate.wuhanddc.DDCSdkClient;
import com.reddate.wuhanddc.dto.ddc.BaseEventBean;
import com.reddate.wuhanddc.dto.ddc.DDC1155TransferBatchEventBean;
import com.reddate.wuhanddc.dto.ddc.DDC1155TransferSingleEventBean;
import com.reddate.wuhanddc.dto.ddc.DDC721TransferEventBean;
import com.reddate.wuhanddc.dto.wuhanchain.TransactionsBean;
import com.reddate.wuhanddc.service.BlockEventService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.klock.annotation.Klock;
import org.springframework.stereotype.Service;
import org.web3j.protocol.core.methods.response.TransactionReceipt;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;

import static com.reddate.wuhanddc.util.CommonUtils.parseHexStr2Int;


@Service
@Slf4j
@AllArgsConstructor
public class DDCServiceImpl implements DDCService {

	private DDCSdkClient dDCSdkClient;

	private TransactionMapper transactionMapper;

	private final AccountService accountService;

	private final String SENDER = "0xb96a0b55052c5d081eed749161b1dc2176b5a61f";

	private final String BLACK_HOLE = "0x49cb0e9a80887e89255e005fc7919b598f5b0851";

	private final DdcTransferRequestMapper ddcTransferRequestMapper;

	private final DdcBalanceMapper ddcBalanceMapper;

	private final AccountMapper accountMapper;

	private final DdcBalanceService ddcBalanceService;

	private final DdcTransferService ddcTransferService;


	public static long parseHexStr2Long(String str) {
		if (StringUtils.isBlank(str)) {
			return 0;
		}
		return Long.parseLong(str.substring(2), 16);
	}


	@Override
	public CoinDDCResultDto coin(CoinDDCDto coinDDCDto) throws Exception {
		String txHash = safeMint(coinDDCDto, SENDER);
		if (!txHash.equals("")) {
			Long ddcId = getDDCIdByTxHash(txHash);
			if (ddcId != null) {
				Transaction transaction = this.saveTransaction(txHash);
				CoinDDCResultDto result = new CoinDDCResultDto();
				BeanUtils.copyProperties(coinDDCDto, result);
				result.setDdcid(ddcId);
				result.setTxHash(txHash);
				result.setUri(coinDDCDto.getUri());
				result.setTxId(transaction.getId());
				saveDdcTransfer(result, transaction.getBlockNumber());
				saveDdcBalance(result);
				return result;
			}
		}
		throw new Exception("铸币失败");
	}

	private void saveDdcTransfer(CoinDDCResultDto coinDDCResultDto, Long blockNumber) {
		DdcTransfer ddcTransfer = DdcTransfer.builder()
				.token(coinDDCResultDto.getDdcid() + "")
				.fromAddress(SENDER)
				.toAddress(SENDER)
				.txTime(LocalDateTime.now())
				.txHash(coinDDCResultDto.getTxHash())
				.blockNumber(blockNumber)
				.copies(coinDDCResultDto.getCopies())
				.ddcType(1155)
				.type(1)
				.build();
		ddcTransferService.save(ddcTransfer);
	}

	private void saveDdcBalance(CoinDDCResultDto coinDDCResultDto) {
		DdcBalance balance = ddcBalanceService.findByOwnerAndType(SENDER, 1155);
		if (balance != null) {
			balance.setCopies(balanceOf(SENDER, coinDDCResultDto.getDdcid() + ""));
			ddcBalanceService.updateById(balance);
		} else {
			balance = DdcBalance.builder()
					.ddcType(1155)
					.token(coinDDCResultDto.getDdcid() + "")
					.ownerAddress(SENDER)
					.copies(balanceOf(SENDER, coinDDCResultDto.getDdcid() + ""))
					.build();
			ddcBalanceService.save(balance);
		}
	}

	@Override
	public SellOutDto sell(SellInDto sellInDto) throws Exception {
		TransferDto transferDto = TransferDto.builder()
				.from(SENDER)
				.to(sellInDto.getToAddress())
				.copies(sellInDto.getCopies())
				.token(sellInDto.getToken())
				.requestId(UUID.randomUUID().toString())
				.build();
		DdcTransferRequest request = transferByRequest(generateTransferRequest(transferDto));
		if (request.getIsSuccess()) {
			SellOutDto result = new SellOutDto();
			BeanUtils.copyProperties(sellInDto, result);
			result.setTxHash(request.getTxHash());
			return result;
		} else {
			throw new Exception(request.getMessage());
		}
	}


	@Override
	public TransferOutDto transfer(TransferInDto transferInDto) throws Exception {
		TransferDto transferDto = TransferDto.builder()
				.from(transferInDto.getFromAddress())
				.to(transferInDto.getToAddress())
				.copies(transferInDto.getCopies())
				.token(transferInDto.getToken())
				.requestId(UUID.randomUUID().toString())
				.build();
		DdcTransferRequest request = transferByRequest(generateTransferRequest(transferDto));
		if (request.getIsSuccess()) {
			TransferOutDto result = new TransferOutDto();
			BeanUtils.copyProperties(transferInDto, result);
			result.setTxHash(request.getTxHash());
			return result;
		} else {
			throw new Exception(request.getMessage());
		}
	}


	@Override
	public BurnOutDto burn(BurnInDto burnInDto) throws Exception {
		TransferDto transferDto = TransferDto.builder()
				.from(burnInDto.getOwnerAddress())
				.to(BLACK_HOLE)
				.copies(burnInDto.getCopies())
				.token(burnInDto.getToken())
				.requestId(UUID.randomUUID().toString())
				.build();
		DdcTransferRequest request = transferByRequest(generateTransferRequest(transferDto));
		if (request.getIsSuccess()) {
			BurnOutDto result = new BurnOutDto();
			BeanUtils.copyProperties(burnInDto, result);
			result.setTxHash(request.getTxHash());
			return result;
		} else {
			throw new Exception(request.getMessage());
		}
	}

	@Klock(keys = {"#sender"})
	private String safeMint(CoinDDCDto coinDDCDto, String sender) throws Exception {
		Map<String, Object> uri = new HashMap<>();
		uri.put("spuId", coinDDCDto.getSpuId());
		uri.put("spuName", coinDDCDto.getSpuName());
		uri.put("brandId", coinDDCDto.getBrandId());
		uri.put("brandName", coinDDCDto.getBrandName());
		uri.put("url", coinDDCDto.getUri());
		byte[] data = "".getBytes(StandardCharsets.UTF_8);
		BigInteger copies = BigInteger.valueOf(coinDDCDto.getCopies());
		String txHash = dDCSdkClient.ddc1155Service.safeMint(sender, sender, copies, JSONObject.toJSONString(uri), data);
		//等待交易成功
		if (awaitTransactionStatus(txHash)) {
			return txHash;
		} else {
			return "";
		}
	}

	private boolean awaitTransactionStatus(String txHash) throws Exception {
		long currentTime = System.currentTimeMillis();
		while (System.currentTimeMillis() - currentTime <= 20_000) {
			try {
				if (dDCSdkClient.baseService.getTransStatusByHash(txHash)) {
					return true;
				}
			} catch (Exception e) {
				log.debug(e.getMessage());
			}
			Thread.sleep(500);
		}
		return false;
	}


	private Long getDDCIdByTxHash(String txHash) throws Exception {
		TransactionReceipt transactionReceipt = dDCSdkClient.baseService.getTransactionReceipt(txHash);
		if (transactionReceipt != null) {
			BlockEventService blockEventService = new BlockEventService();
			ArrayList<BaseEventBean> blockEvent = blockEventService.getBlockEvent(transactionReceipt.getBlockNumber());
			BaseEventBean baseEventBean = blockEvent
					.stream()
					.filter(eventBean ->
							eventBean.getBlockHash().equals(txHash) &&
									(eventBean instanceof DDC721TransferEventBean
											|| eventBean instanceof DDC1155TransferSingleEventBean
											|| eventBean instanceof DDC1155TransferBatchEventBean))
					.findFirst().get();
			if (baseEventBean instanceof DDC721TransferEventBean) {
				return ((DDC721TransferEventBean) baseEventBean).getDdcId().longValue();
			} else if (baseEventBean instanceof DDC1155TransferSingleEventBean) {
				return ((DDC1155TransferSingleEventBean) baseEventBean).getDdcId().longValue();
			}
		}
		return null;
	}

	private Transaction saveTransaction(String txHash) throws Exception {
		TransactionsBean transactionBean = dDCSdkClient.baseService.getTransactionByHash(txHash);
		TransactionReceipt transactionReceipt = dDCSdkClient.baseService.getTransactionReceipt(txHash);
		Transaction transaction = Transaction.builder()
				.blockNumber(parseHexStr2Long(transactionBean.getBlockNumber()))
				.blockHash(transactionBean.getBlockHash())
				.txHash(transactionBean.getHash())
				.toAddress(transactionBean.getFrom())
				.fromAddress(transactionBean.getTo())
				.nonce(parseHexStr2Long(transactionBean.getNonce()))
				.txTime(LocalDateTime.now())
				.gasUsed(transactionReceipt.getGasUsed().longValue())
				.gasPrice(parseHexStr2Long(transactionBean.getGasPrice()))
				.txFee(transactionReceipt.getGasUsed().longValue() * parseHexStr2Long(transactionBean.getGasPrice()))
				.type(parseHexStr2Int(transactionBean.getValue()))
				.input(transactionBean.getInput())
				.gas(parseHexStr2Long(transactionBean.getGas()))
				.build();
		transactionMapper.insert(transaction);
		return transaction;
	}

	@Override
	public String setApprovalForAll() throws Exception {
		return dDCSdkClient.ddc1155Service.setApprovalForAll(SENDER, "0x4e24a6fd1057cdad259ec66b934bd250a1e6f056", true);
	}

	private void saveDdcTransfer(DdcTransferRequest request) {
		DdcTransfer ddcTransfer = DdcTransfer.builder()
				.token(request.getToken())
				.fromAddress(request.getFromAddress())
				.toAddress(request.getToAddress())
				.txTime(LocalDateTime.now())
				.txHash(request.getTxHash())
				.blockNumber(request.getBlockNumber())
				.copies(request.getCopies())
				.ddcType(1155)
				.type(2)
				.build();
		ddcTransferService.save(ddcTransfer);
	}

	private void saveDdcBalance(DdcTransferRequest request) {
		DdcBalance from = ddcBalanceService.findByOwnerAndType(request.getFromAddress(), 1155);
		if (from != null) {
			from.setCopies(balanceOf(request.getFromAddress(), request.getToken()));
			ddcBalanceService.updateById(from);
		} else {
			from = DdcBalance.builder()
					.ddcType(1155)
					.token(request.getToken())
					.ownerAddress(request.getFromAddress())
					.copies(balanceOf(request.getFromAddress(), request.getToken()))
					.build();
			ddcBalanceService.save(from);
		}
		DdcBalance to = ddcBalanceService.findByOwnerAndType(request.getToAddress(), 1155);
		if (to != null) {
			to.setCopies(balanceOf(request.getToAddress(), request.getToken()));
			ddcBalanceService.updateById(to);
		} else {
			to = DdcBalance.builder()
					.ddcType(1155)
					.token(request.getToken())
					.ownerAddress(request.getToAddress())
					.copies(balanceOf(request.getToAddress(), request.getToken()))
					.build();
			ddcBalanceService.save(to);
		}
	}


	@Override()
	@Klock(keys = {"#request.fromAddress"})
	public DdcTransferRequest transferByRequest(DdcTransferRequest request) {
		if (!request.getIsSuccess()) {
			try {
				switch (request.getStatus()) {
					case 0:
						request.setIsFinish(true);
						break;
					case 1:
						if (!Objects.equals(request.getFromAddress(), SENDER)) {
							accountService.rechargeBusiness(request.getFromAddress(), new BigDecimal("0.3"));
							request.setStatus(2);
						}
					case 2:
						if (!Objects.equals(request.getFromAddress(), SENDER)) {
							accountService.rechargeGas(request.getFromAddress(), new BigDecimal("0.1"));
							request.setStatus(3);
						}
					case 3:
						this.checkBalance(request);
						String txHash = dDCSdkClient.ddc1155Service.safeTransferFrom(
								request.getFromAddress(), request.getFromAddress(), request.getToAddress(),
								BigInteger.valueOf(Long.parseLong(request.getToken())),
								BigInteger.valueOf(request.getCopies()), "".getBytes()
						);
						request.setStatus(4);
						request.setTxHash(txHash);
					case 4:
						if (awaitTransactionStatus(request.getTxHash())) {
							Transaction tx = this.saveTransaction(request.getTxHash());
							request.setStatus(5);
							request.setBlockNumber(tx.getBlockNumber());
							request.setTxId(tx.getId());
						} else {
							request.setMessage("等待区块链交易状态超时!");
						}
					case 5:
						saveDdcTransfer(request);
						request.setStatus(6);
					case 6:
						saveDdcBalance(request);
						request.setStatus(7);
					case 7:
						request.setIsFinish(true);
						request.setIsSuccess(true);
				}
			} catch (Exception e) {
				request.setMessage(e.getMessage());
			} finally {
				request.setIsFinish(true);
				ddcTransferRequestMapper.updateById(request);
			}
		}
		return request;
	}

	private DdcTransferRequest getDdcTransferRequest(TransferDto transferDto) {
		LambdaQueryWrapper<DdcTransferRequest> queryWrapper = Wrappers.lambdaQuery();
		queryWrapper.eq(DdcTransferRequest::getRequestId, transferDto.getRequestId());
		List<DdcTransferRequest> requestList = ddcTransferRequestMapper.selectList(queryWrapper);
		if (!requestList.isEmpty()) {
			DdcTransferRequest request1 = requestList.get(0);
			request1.setRetries(request1.getRetries() + 1);
			if (verification(request1)) {
				request1.setStatus(1);
			}
			ddcTransferRequestMapper.updateById(request1);
			return request1;
		} else {
			DdcTransferRequest request = new DdcTransferRequest();
			request.setFromAddress(transferDto.getFrom());
			request.setToAddress(transferDto.getTo());
			request.setCopies(transferDto.getCopies());
			request.setDdcType(1155);
			request.setToken(transferDto.getToken());
			request.setRequestId(transferDto.getRequestId());
			request.setExtraData(transferDto.getExchangeName() + "->" + transferDto.getNotifyRoute());
			request.setMessage("");
			request.setStatus(0);
			request.setIsFinish(false);
			request.setIsSuccess(false);
			request.setRetries(0);
			if (verification(request)) {
				request.setStatus(1);
			}
			ddcTransferRequestMapper.insert(request);
			return request;
		}
	}

	private int balanceOf(String ownerAddress, String token) {
		BigInteger bigInteger = BigInteger.valueOf(Long.parseLong(token));
		try {
			return dDCSdkClient.ddc1155Service.balanceOf(ownerAddress, bigInteger).intValue();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private boolean verification(DdcTransferRequest request) {
		boolean result = true;
		try {
			dDCSdkClient.baseService.checkAccAddr(request.getFromAddress());
			dDCSdkClient.baseService.checkAccAddr(request.getToAddress());
			dDCSdkClient.baseService.checkDdcId(BigInteger.valueOf(Long.parseLong(request.getToken())));
		} catch (Exception e) {
			request.setMessage(e.getMessage());
			result = false;
		}
		if (request.getFromAddress().equals(request.getToAddress())) {
			request.setMessage("交易发起方与接收方地址不能相同");
			result = false;
		}
		if (accountService.findByAddress(request.getFromAddress()) == null) {
			request.setMessage("发起方账户不存在!");
			result = false;
		}
		if (accountService.findByAddress(request.getToAddress()) == null) {
			request.setMessage("接收方账户不存在!");
			result = false;
		}
		return result;
	}

	private void checkBalance(DdcTransferRequest request) {
		int balance = balanceOf(request.getFromAddress(), request.getToken());
		if (balance < request.getCopies()) {
			throw new RuntimeException("DDC余额不足");
		}
	}

	@Override
	public DdcTransferRequest generateTransferRequest(TransferDto transferDto) {
		return getDdcTransferRequest(transferDto);
	}
}


